# -*- coding: utf-8 -*-
# FileName:     CadoHero.py
# time:         23/4/23 023 下午 3:34
# Author:       Zhou Hang
# Description:  按下键盘发出不同的声音
import threading
from queue import Queue  # 线程安全的队列
import keyboard

from src.utils.common import NORMALKEY_MAPPER_LEN, NORMALKEY_MAPPER, MIDIKEY_LEN, NORMALKEY_RANGE, MIDIKEY_RANGE, \
    MIDI_DEVICE_BUFFER_SIZE, NOTE_ON, NOTE_OFF, RESOURCE_DIR, TIMBRE_CHANGE, MOD, MOD_ON, RECORD, EXIT, MAXVOL
from src.core.instruments import GuitarString, Harp, SineWave, SquareWave, TriangularWave, Drum
from src.utils.zmidi import PygameMidi

import platform
if platform.system() == 'Windows':
    from src.utils.zplot import ZPlotEmpty as ZPlot
    from src.utils.zaudio import StdAudio
elif platform.system() == 'Linux':
    from src.utils.zplot import ZPlot, ZPlotEmpty
    from src.utils.zaudio2 import StdAudio


class InstrumentManagement:
    def __init__(self, instrument: str = "Sine", with_midi_keyboard: bool = False):
        """
        管理音色的类
        :param instrument: 默认使用Sine音色
        """
        self.support_instruments = {  # 以后需要添加新的音色, 在这里修改
            "Sine": SineWave,
            "Square": SquareWave,
            "Triangle": TriangularWave,
            "Guitar": GuitarString,
            "Harp": Harp,
            "Drum": Drum
        }
        self.support_instruments_name = list(self.support_instruments.keys())  # 记录所有乐器名称列表
        if instrument not in self.support_instruments.keys():
            print(f"系统当前支持{self.support_instruments.keys()}")
            exit(0)

        # 当前音色的名称和编号
        self.instrument = instrument
        self.instrument_id = self.support_instruments_name.index(instrument)  # 配合上下来切换音色

        # 待初始化的音符长度和范围
        if not with_midi_keyboard:
            self.nr_string = NORMALKEY_MAPPER_LEN
            self.key_range = NORMALKEY_RANGE
        else:
            self.nr_string = MIDIKEY_LEN
            self.key_range = MIDIKEY_RANGE

        # 记录所有音色, 但是等到用到了再初始化
        self.instruments = {}
        for key in self.support_instruments_name:
            self.instruments[key] = []
        self.init_instrument()

    def init_instrument(self):
        """
        根据self.instruments的值进行音色初始化
        根据self.nr_string, self.key_range决定初始化哪些音
        :return:
        """
        if len(self.instruments[self.instrument]) == self.nr_string:
            return  # 已经初始化过该音色, 直接退出即可
        for i in range(*self.key_range):
            # 根据self.key_range初始化音色
            frequency = PygameMidi.get_frequency(i)  # 直接得到对应频率
            self.instruments[self.instrument].append(  # 这里其实不同音色还可以微调, 但是调整的参数不一致, 在这里难以管理
                self.support_instruments[self.instrument](frequency=frequency)  # 根据频率初始化乐器
            )

    def switch_instrument(self, instrument_id):
        """根据乐器id切换乐器音色, 并返回音色名称"""
        self.instrument_id = instrument_id
        self.instrument = self.support_instruments_name[instrument_id]
        self.init_instrument()
        print(f"音色切换至: {self.instrument}")
        return self.instrument

    def next_instrument(self):
        """切换到下一个乐器"""
        next_instrument_id = (self.instrument_id + 1) % len(self.support_instruments_name)
        return self.switch_instrument(next_instrument_id)

    def prev_instrument(self):
        """切换到上一个乐器"""
        prev_instrument_id = (self.instrument_id - 1) % len(self.support_instruments_name)
        return self.switch_instrument(prev_instrument_id)

    def get_instrument(self) -> list:
        """获得当前乐器的所有音符"""
        return self.instruments[self.instrument]

    def get_nr_string(self) -> int:
        """获得音符的个数"""
        return self.nr_string

    def get_instrument_id(self):
        """返回当前乐器编号"""
        return self.instrument_id

    def max_instrument_id(self):
        """返回最大乐器编号"""
        return len(self.support_instruments_name)


class CadoHero:
    def __init__(self, zplot, instrument: str = "Sine", with_midi_keyboard: bool = False):
        """
        :param instrument: 默认使用Sine音色
        :param with_midi_keyboard: 默认不打开midi键盘设备
        """
        self.time_to_exit = False  # 控制什么时候退出整个系统
        self.with_midi_keyboard = with_midi_keyboard  # 是否打开midi设备
        if not with_midi_keyboard:
            self.keyboard_mapper = NORMALKEY_MAPPER
        self.instrument_mgt = InstrumentManagement(instrument, with_midi_keyboard)
        self.instrument = self.instrument_mgt.get_instrument()  # 该音色的所有notes

        self.note_queue = Queue()  # 为了实现和弦功能，设计一个线程安全的队列，存放同时按下的按键

        self.zmidi = PygameMidi()
        if with_midi_keyboard:
            self.zmidi.open_device()

        self.vol = {}  # 所有音符当前的音量, 这是同时发音的关键

        self.zaudio = StdAudio()
        self.save_mode = False  # 当前是不是保存模式的状态位

        self.zplot = zplot
        print("初始化完成，可以开始演奏")

    def sample(self):
        """
        根据self.vol取样当前需要的发声的弦
        :return:
        """
        sample_ = 0
        vols = self.vol.copy()
        for key in vols.keys():
            sample_ += (self.vol.get(key, 0) * self.instrument[key].sample())
        return sample_

    def tic(self):
        """
        震动当前正在发声的弦
        :return:
        """
        vols = self.vol.copy()
        for key in vols.keys():
            self.instrument[key].tic()

    def play(self):
        """
        播放当前正在发声的弦
        :return:
        """
        self.zaudio.play_float(self.sample())
        self.tic()

    def next_instrument(self):
        self.instrument_mgt.next_instrument()
        self.instrument = self.instrument_mgt.get_instrument()

    def prev_instrument(self):
        self.instrument_mgt.prev_instrument()
        self.instrument = self.instrument_mgt.get_instrument()

    def switch_instrument(self, instrument_id: int):
        instrument_name = self.instrument_mgt.switch_instrument(instrument_id)
        self.instrument = self.instrument_mgt.get_instrument()
        return instrument_name

    def start_normal_keyboard(self):
        """
        使用普通键盘演奏
        :return:
        """
        assert not self.with_midi_keyboard

        def callback(x):
            try:
                if x is None:
                    return
                if x.name == "up":  # 切换到上一个乐器
                    self.next_instrument()
                elif x.name == "down":  # 切换到下一个乐器
                    self.prev_instrument()
                if x.name == 'space': x.name = ' '
                index = self.keyboard_mapper.find(x.name)
                if index != -1:
                    self.current_key_index = index
                    self.pluck()
            except TypeError:
                print("TypeError:", x)

        keyboard.on_press(callback)
        while True:
            self.play()

    def __midi_read_t(self):
        """
        在线程中调用, 读取midi键盘输入, 并执行对应操作
        :return:
        """
        while not self.time_to_exit:
            if self.zmidi.poll():
                data = self.zmidi.read(MIDI_DEVICE_BUFFER_SIZE)  # 读取键盘输入
                print(data)  # 测试
                midi_event, timestamp = data[0]  # 根据测试，仅在极少数情况下len(data)会超过1，这可能是我的键盘太便宜了，很难扫描到同时按下的按键
                if midi_event[0] == NOTE_ON:
                    target_note_index = midi_event[1] - MIDIKEY_RANGE[0]
                    self.vol[target_note_index] = midi_event[2] / MAXVOL
                    self.instrument[target_note_index].pluck()
                    print(self.vol)
                elif midi_event[0] == NOTE_OFF:
                    target_note_index = midi_event[1] - MIDIKEY_RANGE[0]
                    self.vol.pop(target_note_index, 0)
                elif midi_event[0] == TIMBRE_CHANGE:
                    instrument_id = midi_event[1]  # 获得乐器编号
                    instrument_id = min(instrument_id, self.instrument_mgt.max_instrument_id() - 1)
                    instrument_name = self.switch_instrument(instrument_id)
                    self.zplot.display(f"{RESOURCE_DIR}/{instrument_name}.jpg")  # 修改音色图像
                elif midi_event[0] == MOD and midi_event[2] == MOD_ON:  # 功能按键模式
                    if midi_event[1] == RECORD:
                        # 切换录音模式, 同时显示ui
                        if not self.save_mode:
                            self.zplot.display(f"{RESOURCE_DIR}/recording.jpg")
                        self.zaudio.set_save_mode(not self.save_mode)
                        self.save_mode = not self.save_mode
                        if not self.save_mode:
                            self.zplot.display(f"{RESOURCE_DIR}/finish.jpg")
                    elif midi_event[1] == EXIT:
                        self.time_to_exit = True
        print("Midi键盘读取线程退出")
        exit(0)

    def start_midi_keyboard(self):
        """
        使用midi键盘演奏
        :return:
        """
        assert self.with_midi_keyboard
        # 开启一个线程不断读取midi键盘信息，并设置相应变量
        midi_read_t = threading.Thread(target=self.__midi_read_t)
        midi_read_t.start()

        while not self.time_to_exit:
            self.play()

        self.clear()
        exit(0)

    def clear(self):
        self.zaudio.close()
        self.zmidi.close()
        print("主线程退出")


def main():
    # cado = CadoHero(with_midi_keyboard=False)
    # cado.start_normal_keyboard()

    cado = CadoHero(zplot=ZPlot(), with_midi_keyboard=True)
    cado.start_midi_keyboard()


if __name__ == "__main__":
    main()
